Skip to content

Conversation

thecrypticace
Copy link
Contributor

@thecrypticace thecrypticace commented Oct 3, 2025

Loading Oxide's .node file into the language server process / VSCode extension host on Windows marks that .node file as in use. When this happens you cannot completely delete node_modules (for example by running npm ci).

To work around this we'll fork a new process that can load Oxide, run its scan(s), and then exit. During initial project discovery we use a temporarily long lived process that persists for the duration of project discovery — which may include multiple Oxide scans across projects (and even versions). Once the project discovery has completed the process will exit. For any subsequent content scanning (e.g. when CSS changes) we'll spawn a new, temporary process for that individual scan.

This will ensure that, once the process has exited, the .node file is no longer considered to be in-use and commands like npm ci will run properly.

Commentary

Why not use require.cache?

Unfortunately, deleting entries from require.cache does not unload the Oxide binary from the process address space.

Why not worker threads

So, this might work but also might not. You'd still be loading it into the processes address space so there's a chance that as long as the process is open — whether the thread has exited or not — the .node file would still be marked as in use.

Additionally, we have some flags set when building Oxide that basically prevent it from unloading in worker threads due to some bugs in the Rust standard library. This applies to Linux only iirc so it shouldn't actually be a problem there but I'd rather keep the mechanism working consistently across operating systems.

Communication between processes

The main process and helper communicate using a JSON-RPC protocol — similar to the one used by language servers/clients — but without any initialization setup. This is an internal tool and the message format is not considered stable and may change in any future version.

Communication happens over an IPC channel provided by child_process.fork(…). As far as I am aware, this uses private file descriptors shared between processes. No other process should be capable of "tricking" the helper into loading other .node files into its address space. Only the ones we discover during NPM package resolution should ever be loaded. Even though the temporary helper isn't active for very long this was still a concern I had while developing this.

Test Plan

There are automated tests that verify existing functionality still works but testing this specific scenario on Windows in an automated fashion with the current test setup would be a bit annoying so I did some additional manual testing:

  • Set up a small Tailwind CSS v4 project on Windows
  • Opened VS Code with the current version of the extension
  • Ran npm ci in the terminal and watched it fail b/c of the Oxide .node file
  • Loaded the new extension through the extension development host
  • Repeated the steps
  • Ran npm ci multiple times to make sure it worked

@thecrypticace thecrypticace marked this pull request as draft October 3, 2025 01:52
@DustinsCode
Copy link

I can pull it down at work on my windows machine tomorrow to try it out and report back!

@thecrypticace
Copy link
Contributor Author

thecrypticace commented Oct 3, 2025

So it looks like this isn't possible from my (somewhat limited) testing. Node supposedly handles the unloading of these files but I'm guessing it just doesn't on Windows.

If you run this script:

let mod = require("./node_modules/@tailwindcss/oxide-win32-arm64-msvc/tailwindcss-oxide.win32-arm64-msvc.node")

console.log(mod);
mod = null
delete require.cache["./node_modules/@tailwindcss/oxide-win32-arm64-msvc/tailwindcss-oxide.win32-arm64-msvc.node"];

setTimeout(() => {}, 60_000)

and then try to run npm ci it will fail so I think our only option here is process forking + IPC. This is kinda annoying to have to do but it's probably the only real way to do this so. 🤷‍♂️

@thecrypticace thecrypticace changed the title Unload Oxide binary on Windows after use Offload Oxide scanning to separate process Oct 3, 2025
@thecrypticace thecrypticace marked this pull request as ready for review October 9, 2025 21:00
@thecrypticace
Copy link
Contributor Author

@DustinsCode If you wanna give this PR a go it should work (does in my testing)

Comment on lines +34 to +35
// TODO: Can we find a way to not require a build first?
// let module = path.resolve(path.dirname(__filename), './oxide-helper.ts')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm leaving this in as a note for me to look into later. Right now editing the helper file and re-running tests requires a rebuild. But this is "fine" for now.

CI already does this (b/c a few tests have to actually test against the built version for reasons).

Didn’t mean to leave this in
These are expected if you are just using the plain `tailwindcss` package or don’t have `tailwindcss` installed locally at all — for example when using the Standalone CLI.

Other errors should be surfaced though as they could indicate a problem.
@thecrypticace thecrypticace merged commit 3f0655f into main Oct 10, 2025
12 checks passed
@thecrypticace thecrypticace deleted the fix/issue-1470 branch October 10, 2025 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants